<?php
//
// +------------------------------------------------------------------------+
// | Gecko Framework                                                        |
// +------------------------------------------------------------------------+
//
require_once( 'Gecko/Security/Exception.php' );

/**
 * Checks against a ACL with a Provider, to see
 * if the user is clear to enter/or perform action
 *
 * @package Gecko
 * @author Christopher Valderrama <gatorv@gmail.com>
 * @copyright Copyright (c) 2007-2008
 * @version $Id$
 * @access public
 */
class Gecko_Security implements Gecko_Security_Interface {
	/**
	 * The action to perform, aka
	 * the privilege
	 *
	 * @var string
	 */
	private $action = null;
	/**
	 * The controller the user is
	 * requesting, aka the Resource
	 *
	 * @var string
	 */
	private $resource = null;
	/**
	 * Checks if the ACL has been
	 * constructed
	 *
	 * @var boolean
	 */
	private $aclConstructed = false;
	/**
	 * The ACL check list
	 *
	 * @see Zend_Acl
	 * @var Zend_Acl
	 */
	protected $acl = null;
	/**
	 * The security provider
	 *
	 * @var Gecko_Security_Provider_Interface
	 */
	protected $provider = null;
	/**
	 * The result of validation
	 *
	 * @var boolean
	 */
	protected $_result = false;
	/**
	 * The roles for the security system
	 *
	 * @var array
	 */
	protected $_roles;
	/**
	 * The resources for the security system
	 *
	 * @var array
	 */
	protected $_resources;
	/**
	 * The Zend_Cache Object to cache the ACL
	 *
	 * @var Zend_Cache_Core
	 */
	protected $_cache;
	/**
	 * The id for the security object
	 *
	 * @var string
	 */
	protected $_id = 'security';

	/**
	 * Initiates a new Security object, it can
	 * optionally be initiated with a specific $resource
	 * and a specific $action
	 *
	 * @param string $resource The active resource
	 * @param string $action The action to perform
	 */
	public function __construct( $resource = '', $action = '' ) {
		$this->resource = $resource;
		$this->action = $action;
		$this->acl = new Zend_Acl();
		$this->_cache = null;
	}

	/**
	 * Sets the cache object to use for ACL
	 *
	 * @param Zend_Cache_Core $cache The cache object
	 */
	public function setCache(Zend_Cache_Core $cache) {
		$this->_cache = $cache;
	}

	/**
	 * Sets the security id
	 *
	 * @param string $id
	 */
	public function setSecurityId($id) {
		$this->_id = $id;
	}

	/**
	 * Returns the security id
	 *
	 * @return string
	 */
	public function getSecurityId() {
		return $this->_id;
	}

	/**
	 * Returns the cache object used by the class
	 *
	 * @return Zend_Cache_Core
	 */
	public function getCache() {
		return $this->_cache;
	}

	/**
	 * Code to execute before checking
	 */
	protected function preCheck() {}

	/**
	 * Code to execute after checking
	 */
	protected function postCheck() {}

	/**
	 * Returns the Roles setup by provider
	 *
	 * @return array
	 */
	public function getRoles() {
		return $this->_roles;
	}

	/**
	 * Checks if a role exists
	 *
	 * @param string $role
	 * @return boolean
	 */
	public function hasRole($role) {
		foreach($this->_roles as $_role) {
			if( $_role === $role ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns the resources setup by provider
	 *
	 * @return array
	 */
	public function getResources() {
		return $this->_resources;
	}

	/**
	 * Checks if a resource exist
	 *
	 * @param string $resource
	 * @return boolean
	 */
	public function hasResource($resource) {
		foreach($this->_resources as $_resource) {
			if($_resource === $resource) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns the sent controller name
	 *
	 * @return string
	 */
	public function getResource() {
		return $this->resource;
	}

	/**
	 * Sets the active resource
	 *
	 * @param string $resource
	 * @return self for chaining
	 */
	public function setResource($resource) {
		$this->resource = $resource;

		return $this;
	}

	/**
	 * Returns the action to perform
	 *
	 * @return string
	 */
	public function getAction() {
		return $this->action;
	}

	/**
	 * Sets the action to check
	 *
	 * @param string $action
	 * @return self object for chaining
	 */
	public function setAction($action) {
		$this->action = $action;

		return $this;
	}

	/**
	 * Checks if the user is allowed
	 *
	 * @param string $role The role to execute
	 * @return boolean
	 */
	public function isAllowed($role) {
		$this->preCheck();
		$this->setupACL();

		if( $this->acl->isAllowed($role, $this->resource, $this->action) ) {
			$this->_result = true;
		}

		$this->postCheck();

		return $this->_result;
	}

	/**
	 * Establishes the provider of the ACL
	 *
	 * @param Gecko_Security_Provider_Interface $provider
	 */
	public function setProvider(Gecko_Security_Provider_Interface $provider) {
		$this->provider = $provider;
	}

	/**
	 * Setups the ACL
	 *
	 * A special "all" keyword can be used for any
	 */
	private function setupACL() {
		if( $this->provider == null ) {
			throw new Gecko_Security_Exception('Please set a Security provider before checking permissions');
		}

		if( $this->aclConstructed == true ) {
			return;
		}

		if (null !== $this->_cache) {
			$cacheId = md5($this->_id);
		}

		if (null === $this->_cache || !($data = $this->_cache->load($cacheId))) {
			$roles = $this->provider->getRoles();
			$resources = $this->provider->getResources();
			$acl = $this->provider->getAccessList();

			if (null !== $this->_cache) {
				$data = array();
				$data['roles'] = $roles;
				$data['resources'] = $resources;
				$data['acl'] = $acl;

				if(!$this->_cache->save($data, $cacheId)) {
                	throw new Gecko_Security_Exception('Failed saving acl settings to cache');
				}
            }
		} else {
			$roles = $data['roles'];
			$resources = $data['resources'];
			$acl = $data['acl'];
		}

		foreach( $roles as $role ) {
			$this->acl->addRole( new Zend_Acl_Role( $role ) );
		}

		foreach( $resources as $resource ) {
			$this->acl->add( new Zend_Acl_Resource( $resource ) );
		}

		foreach( $acl as $list ) {
			$resource = $list['resource']; // Group name
			$users = $list['users']; // Array of users

			foreach( $users as $user ) {
				$role = $user['role']; // Must be defined as a Role or 'all'
				$privileges = $user['privileges']; // Array of privilages or 'all'

				if( $role == "all" ) {
					$role = null;
				}

				if( $privileges === "all" ) {
					$privileges = null;
				} else {
					if( !is_array( $privileges ) ) {
						throw new Gecko_Security_Exception('$privileges expected to be a array, ' . gettype($privileges) . ' given');
					}
				}

				$this->acl->allow( $role, $resource, $privileges );
			}
		}

		$this->_roles = $roles;
		$this->_resources = $resources;
		$this->aclConstructed = true;
		$this->postSetup();
	}

	/**
	 * Additional setup of ACL
	 *
	 * @return void
	 */
	protected function postSetup() {

	}

	/**
	 * Returns the Zend_Acl object
	 *
	 * @return Zend_Acl The current Acl
	 */
	public function getACL() {
		return $this->acl;
	}
}
